/**
 * Copyright (c) 2011-2021, JFXtras
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *    Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *    Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *    Neither the name of the organization nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL JFXTRAS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package jfxtras.internal.scene.control.skin;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.css.converter.EnumConverter;
import javafx.scene.control.SkinBase;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.stage.Popup;
import javafx.util.Callback;
import jfxtras.css.CssMetaDataForSkinProperty;
import jfxtras.css.converters.SimpleDateFormatConverter;
import jfxtras.scene.control.CalendarPicker;
import jfxtras.scene.control.CalendarPicker.CalendarRange;
import jfxtras.scene.control.CalendarTextField;
import jfxtras.scene.control.ImageViewButton;
import jfxtras.scene.control.LocalDatePicker;
import jfxtras.scene.control.LocalDateTextField;
import jfxtras.scene.control.LocalDateTimePicker;
import jfxtras.scene.control.LocalDateTimeTextField;
import jfxtras.scene.layout.VBox;
import jfxtras.util.NodeUtil;

/**
 * 
 * @author Tom Eugelink
 * 
 * Possible extension: drop down list or grid for quick selection
 */
public class CalendarTextFieldSkin extends SkinBase<CalendarTextField> implements TextFieldSkin
{
    // ==================================================================================================================
    // CONSTRUCTOR
    
    /**
     * 
     */
    public CalendarTextFieldSkin(CalendarTextField control)
    {
        super(control);
        construct();
    }

    /*
     * 
     */
    private void construct()
    {
        // setup component
        createNodes();
                
        // react to value changes in the model
        getSkinnable().calendarProperty().addListener( (observableValue, oldValue, newValue) -> { refreshValue(); });
        getSkinnable().dateFormatProperty().addListener( (observableValue, oldValue, newValue) -> { refreshValue(); });
        textField.textProperty().bindBidirectional(getSkinnable().textProperty());
        textField.editableProperty().bindBidirectional(getSkinnable().editableProperty());
        refreshValue();

        // The enter event must be forwarded to support the DefaultButton
        textField.addEventHandler(KeyEvent.ANY, keyEvent -> {
            if (keyEvent.getCode() == KeyCode.ENTER) {
                getSkinnable().fireEvent(keyEvent);
            }
        });

        // focus
        initFocusSimulation();
                
        /**
         * If the user is triggering the property, we must show the popup.
         * We cannot bind the property directly to the popup because the
         * popup property is read only. MoreOver, we want to show the popup
         * next to the TextField, that's why we need to call showPopup();
         */
        getSkinnable().pickerShowingProperty().addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean showing) -> {
            if (showing) {
                showPopup();
            } else if(popup != null){
                popup.hide();
            }
        });
    }
    
    /*
     * 
     */
    private void refreshValue()
    {
        // write out to textfield
        Calendar c = getSkinnable().getCalendar();
        String s = c == null ? "" : getSkinnable().getDateFormat().format( c.getTime() );
        textField.setText(s);
        if (s.equals(getSkinnable().getText()) == false) {
            getSkinnable().setText(s);
        }
    }
    
    /**
     * When the control is focus, forward the focus to the textfield
     */
    private void initFocusSimulation() 
    {
           getSkinnable().focusedProperty().addListener((observableValue, wasFocused, isFocused) -> {
            if (isFocused) {
                textField.requestFocus();
            }
        });
    }
        @Override
    public void selectAll(){
            textField.selectAll();
        }

    // ==================================================================================================================
    // DRAW
    
    /**
     * construct the nodes
     */
    private void createNodes()
    {
        // the main textField
        textField = new TextField();
        textField.setPrefColumnCount(10 + (getSkinnable().getShowTime() ? 10 : 0));
        textField.focusedProperty().addListener( (observable) -> {
            if (textField.isFocused() == false) {
                parse();
            }
            // update derived properties on our control 
            focusForwardingProperty.set(textField.focusedProperty().get());
        });
        textField.setOnAction( (actionEvent) ->  {
            parse();
        });
        textField.setOnKeyPressed( (keyEvent) ->  {
            if (keyEvent.getCode() == KeyCode.UP || keyEvent.getCode() == KeyCode.DOWN) {
                // parse the content
                parse();
                
                // get the calendar to modify
                Calendar lCalendar = getSkinnable().getCalendar();
                if (lCalendar == null) return;
                lCalendar = (Calendar)lCalendar.clone();
                
                // modify
                int lField = Calendar.DATE;
                if (keyEvent.isShiftDown() == false && keyEvent.isControlDown()) lField = Calendar.MONTH;
                if (keyEvent.isShiftDown() == false && keyEvent.isAltDown()) lField = Calendar.YEAR;
                if (keyEvent.isShiftDown() == true && keyEvent.isControlDown() &&  getSkinnable().getShowTime()) lField = Calendar.HOUR_OF_DAY;
                if (keyEvent.isShiftDown() == true && keyEvent.isAltDown() &&  getSkinnable().getShowTime()) lField = Calendar.MINUTE;
                lCalendar.add(lField, keyEvent.getCode() == KeyCode.UP ? 1 : -1);
                
                // set it
                getSkinnable().setCalendar(lCalendar);
            }
        });
        // bind the textField's tooltip to our (so it will show up) and give it a default value describing the mutation features
        textField.tooltipProperty().bindBidirectional(getSkinnable().tooltipProperty()); 
        if (getSkinnable().getTooltip() == null)
        {
            // TODO: internationalize the tooltip
            getSkinnable().setTooltip(new Tooltip("Type a date or use # for today, or +/-<number>[d|w|m|y] for delta's (for example: -3m for minus 3 months)\nUse cursor up and down plus optional shift (week), ctrl (month) or alt (year) for quick keyboard changes."));
        }
        textField.promptTextProperty().bind(getSkinnable().promptTextProperty());

        // the icon
        imageView = new ImageViewButton();
        imageView.getStyleClass().add("icon");
        imageView.setPickOnBounds(true);
        imageView.setOnMouseClicked( (evt) -> {
            // no popup if the textfield is not editable (disabled is handled by the node)
            if (!textField.isEditable()) {
                return;
            }
            if (textField.focusedProperty().get() == true) {
                parse();
            }
            getSkinnable().setPickerShowing(true);
        });
        
        // construct a gridpane: one row, two columns
        BorderPane borderPane = new BorderPane();
        borderPane.setCenter(textField);
        borderPane.setRight(imageView);
        getChildren().add(borderPane);

        // add to self
        getSkinnable().getStyleClass().add(this.getClass().getSimpleName()); // always add self as style class, because CSS should relate to the skin not the control
    }
    private TextField textField = null;
    private ImageView imageView = null;
    final public BooleanProperty focusForwardingProperty = new SimpleBooleanProperty();

        /**
     * parse the contents that was typed in the textfield
     */
    private void parse() {
        parse(textField.getText());
    }
    
    /**
     * parse the text
     */
    private void parse(String lText) {
        try {
            // process the text             
            lText = lText.trim();
            if (lText.length() == 0) {
                if (getSkinnable().getAllowNull() == false) {
                    throw new IllegalArgumentException("Empty string would result in null and that is not allowed");
                }
                getSkinnable().setCalendar(null);
                return;
            }
            
            // starts with - means substract days
            if (lText.startsWith("-") || lText.startsWith("+")) {
                // + has problems 
                if (lText.startsWith("+")) lText = lText.substring(1);
                
                // special units Day, Week, Month, Year
                // TODO: internationalize?
                int lUnit = Calendar.DATE;
                if (lText.toLowerCase().endsWith("d")) { lText = lText.substring(0, lText.length() - 1); lUnit = Calendar.DATE; }
                if (lText.toLowerCase().endsWith("w")) { lText = lText.substring(0, lText.length() - 1); lUnit = Calendar.WEEK_OF_YEAR; }
                if (lText.toLowerCase().endsWith("m")) { lText = lText.substring(0, lText.length() - 1); lUnit = Calendar.MONTH; }
                if (lText.toLowerCase().endsWith("y")) { lText = lText.substring(0, lText.length() - 1); lUnit = Calendar.YEAR; }
                
                // parse the delta
                int lDelta = Integer.parseInt(lText);
                Calendar lCalendar = (Calendar)getSkinnable().getCalendar().clone();
                lCalendar.add(lUnit, lDelta);
                
                // set the value
                getSkinnable().setCalendar(lCalendar);
            }
            else if (lText.equals("#")) {
                // set the value
                getSkinnable().setCalendar(Calendar.getInstance(getSkinnable().getLocale()));
            }
            else {
                try
                {
                    Calendar lCalendar = getSkinnable().getCalendar();
                    Date lDate = null;
                    // First we're going to try the parsers in the list.
                    // The user is free to decide to sequence here, if we would try the default first, that would not be the case.
                    for (DateFormat lDateFormat : getSkinnable().getDateFormats()) {
                        try {
                            // parse using the formatter
                            lDate = lDateFormat.parse( lText );
                            break; // exit the for loop
                        }
                        catch (java.text.ParseException | java.time.format.DateTimeParseException e2) {
                            // we can safely ignore this, since we will fall back to the default formatter in the end
                            // TODO: do we want a way to show these error messages? System.out.println(e2.getMessage());
                        } 
                    }
                    if (lDate == null) {
                        // parse using the default formatter
                        lDate = getSkinnable().getDateFormat().parse( lText );
                    }
                    
                    // set the value (the parse with the default formatter either succeeded or threw an exception, skipping this code)
                    lCalendar = Calendar.getInstance(getSkinnable().getLocale());
                    lCalendar.setTime(lDate);
                    getSkinnable().setCalendar(lCalendar);
                }
                finally
                {
                    // always refresh
                    refreshValue();
                }
            }
        }
        catch (Throwable t) 
        { 
            // handle the exception
            // TODO: implement a default handler (show in popup / validation icon) 
            if (getSkinnable().getParseErrorCallback() != null) {
                getSkinnable().getParseErrorCallback().call(t);
            }
            else {
                t.printStackTrace();
            }
        } 
    }
    
    private void setupPopup(){
        // create a picker
        calendarPicker = new CalendarPicker();
        calendarPicker.setMode(CalendarPicker.Mode.SINGLE);
        calendarPicker.localeProperty().set(getSkinnable().localeProperty().get());
        calendarPicker.allowNullProperty().set(getSkinnable().allowNullProperty().get());
        calendarPicker.calendarProperty().set(getSkinnable().calendarProperty().get());
        DateTimeToCalendarHelper.sync(calendarPicker.disabledCalendars(), getSkinnable().disabledCalendars());
        DateTimeToCalendarHelper.sync(calendarPicker.highlightedCalendars(), getSkinnable().highlightedCalendars());
        calendarPicker.displayedCalendar().set(getSkinnable().getDisplayedCalendar());
        calendarPicker.setCalendarRangeCallback(new Callback<CalendarRange,Void>() {
            @Override
            public Void call(CalendarRange calendarRange) {
                Callback<CalendarRange, Void> lCallback = getSkinnable().getCalendarRangeCallback();
                if (lCallback == null) {
                    return null;
                }
                return lCallback.call(calendarRange);
            }
        });
        calendarPicker.setValueValidationCallback(new Callback<Calendar, Boolean>() {
            @Override
            public Boolean call(Calendar calendar) {
                Callback<Calendar, Boolean> lCallback = getSkinnable().getValueValidationCallback();
                if (lCallback == null) {
                    return true;
                }
                return lCallback.call(calendar);
            }
        });
        
        // TODO: replace with PopupControl because that is styleable (see C:/Users/user/Documents/openjfx/8.0rt/modules/controls/src/main/java/com/sun/javafx/scene/control/skin/ComboBoxPopupControl.java)
//            popup = new PopupControl() {
//
//                @Override public Styleable getStyleableParent() {
//                    return CalendarTextFieldSkin.this.getSkinnable();
//                }
//                {
//                    setSkin(new Skin<Skinnable>() {
//                        @Override public Skinnable getSkinnable() { return CalendarTextFieldSkin.this.getSkinnable(); }
//                        @Override public Node getNode() { return lBorderPane; }
//                        @Override public void dispose() { }
//                    });
//                    getScene().getRoot().impl_processCSS(true);
//                }
//            };
        popup = new Popup();
        popup.setAutoFix(true);
        popup.setAutoHide(true);
        popup.setHideOnEscape(true);
        BorderPane lBorderPane = new BorderPane() {
            // As of 1.8.0_40 CSS files are added in the scope of a control, the popup does not fall under the control, so the stylesheet must be reapplied 
            // When JFxtras is based on 1.8.0_40+: @Override 
            public String getUserAgentStylesheet() {
                return getSkinnable().getUserAgentStylesheet();
            }
        };
        lBorderPane.getStyleClass().add(this.getClass().getSimpleName() + "_popup");
        lBorderPane.setCenter(calendarPicker);
        calendarPicker.showTimeProperty().set(getSkinnable().getShowTime());
        
        // because the Java 8 DateTime classes use the CalendarPicker, we need to add some specific CSS classes here to support seamless CSS
        if (getSkinnable().getStyleClass().contains(LocalDateTextField.class.getSimpleName())) {
            calendarPicker.getStyleClass().addAll(LocalDatePicker.class.getSimpleName());
        }
        if (getSkinnable().getStyleClass().contains(LocalDateTimeTextField.class.getSimpleName())) {
            calendarPicker.getStyleClass().addAll(LocalDateTimePicker.class.getSimpleName());
        }
        
        // add a close and accept button if we're showing time
        if ( getSkinnable().getShowTime())
        {
            VBox lVBox = new VBox(); 
            lBorderPane.rightProperty().set(lVBox);
            
            ImageView lAcceptIconImageView = new ImageViewButton();
            lAcceptIconImageView.getStyleClass().addAll("accept-icon");
            lAcceptIconImageView.setPickOnBounds(true);
            lAcceptIconImageView.setOnMouseClicked( (mouseEvent) ->  {
            getSkinnable().calendarProperty().set(calendarPicker.calendarProperty().get());
                if (popup != null) {
                    popup.hide();
                }
            });
            lVBox.add(lAcceptIconImageView);
            
            ImageView lCloseIconImageView = new ImageViewButton();
            lCloseIconImageView.getStyleClass().addAll("close-icon");
            lCloseIconImageView.setPickOnBounds(true);
            lCloseIconImageView.setOnMouseClicked( (mouseEvent) ->  {
                popup.hide(); 
            });
            if (!getSkinnable().isImmediate()) {
                lVBox.add(lCloseIconImageView);
            }
        }
        
        // if a value is selected in date mode, immediately close the popup
        calendarPicker.calendarProperty().addListener( (observable) -> {
            if (getSkinnable().getShowTime() == false && popup.isShowing()) {
                popup.hide(); 
            }
            if (getSkinnable().isImmediate()) {
                getSkinnable().calendarProperty().set(calendarPicker.calendarProperty().get());
            }
        });

        // when the popup is hidden 
        popup.setOnHiding( (windowEvent) -> {
            // and time is not shown, the value must be set into the textfield
            if ( getSkinnable().getShowTime() == false) {
                getSkinnable().calendarProperty().set(calendarPicker.calendarProperty().get());
            }
            // but at least the textfield must be enabled again
            textField.setDisable(false);
            textField.requestFocus();
        });
        
        // add to popup
        popup.getContent().setAll(lBorderPane);
        
        // show it just below the textfield
        textField.setDisable(true);
                
        /**
         * If the popup is showing/hiding, we must notify the property of
         * the CalendarTextField so that they are always in sync.
         */
        popup.showingProperty().addListener(new ChangeListener<Boolean>() {

            @Override
            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean showing) {
                getSkinnable().setPickerShowing(showing);
                if (!showing) {
                    popup.showingProperty().removeListener(this);
                    popup = null;
                }
            }
        });
    
        }
        
        private Popup popup = null;
        private CalendarPicker calendarPicker = null;

    /**
     * This is either called by the user clicking on the button, or
     * programmatically with {@link CalendarTextField#setPickerShowing(boolean)
     * }.
     */
    private void showPopup() {
        if(popup == null){
            setupPopup();
        }
        double height = textField.getHeight() == 0.0 ? textField.prefHeight(-1): textField.getHeight();
        popup.show(textField, NodeUtil.screenX(getSkinnable()), NodeUtil.screenY(getSkinnable()) + height);

        // move the focus over        
        calendarPicker.requestFocus(); // TODO: not working
    }
}
